/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/
package ledger

import (
	reqContext "context"
	"math/rand"
	"time"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/discovery"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/filter"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/verifier"
	contextImpl "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"github.com/golang/protobuf/proto"

	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/status"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/chconfig"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/ledger"
	"github.com/pkg/errors"
)

type Client struct {
	ctx       context.Chain
	filter    ichain.TargetFilter
	ledger    *ledger.Ledger
	verifier  verifier.ResponseVerifier
	discovery ichain.DiscoveryService
}

func New(chainProvider context.ChainProvider, opts ...ClientOption) (*Client, error) {

	chainContext, err := chainProvider()
	if err != nil {
		return nil, err
	}

	if chainContext.ChainService() == nil {
		return nil, errors.New("chain service not initialized")
	}

	membership, err := chainContext.ChainService().Membership()
	if err != nil {
		return nil, errors.WithMessage(err, "membership creation failed")
	}

	// 创建账本对象
	l, err := ledger.NewLedger(chainContext.ChainID())
	if err != nil {
		return nil, err
	}

	endpointFilter := filter.NewEndpointFilter(chainContext, filter.LedgerQuery)

	discoveryService, err := chainContext.ChainService().Discovery()
	if err != nil {
		return nil, err
	}

	disc := discovery.NewFilterDiscovery(discoveryService, endpointFilter)

	ledgerClient := Client{
		ctx:       chainContext,
		ledger:    l,
		verifier:  &verifier.Signature{Membership: membership},
		discovery: disc,
	}

	for _, opt := range opts {
		err := opt(&ledgerClient)
		if err != nil {
			return nil, err
		}
	}

	if ledgerClient.filter == nil {
		if chainContext.Identifier().IAMID == "" {
			return nil, errors.New("iamID not available in user context")
		}
		defFilter := filter.NewIAMFilter(chainContext.Identifier().IAMID)
		ledgerClient.filter = defFilter
	}

	return &ledgerClient, nil
}

// QueryInfo 查询区块信息
func (c *Client) QueryInfo(options ...RequestOption) (*ichain.BlockchainInfoResponse, error) {

	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryInfo failed to prepare request parameters")
	}
	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	responses, err := c.ledger.QueryInfo(reqCtx, targets, c.verifier)
	if err != nil && len(responses) == 0 {
		return nil, errors.WithMessage(err, "QueryInfo failed")
	}

	if len(responses) < opts.MinTargets {
		return nil, errors.Errorf("Number of responses %d is less than MinTargets %d. Targets: %v, Error: %s", len(responses), opts.MinTargets, targets, err)
	}

	response := responses[0]
	maxHeight := response.BCI.Height
	for i, r := range responses {
		if i == 0 {
			continue
		}

		if r.BCI.Height > maxHeight {
			response = r
			maxHeight = r.BCI.Height
		}

	}

	return response, nil
}

// QueryBlockByHash 根据区块hash查询区块信息
func (c *Client) QueryBlockByHash(blockHash []byte, options ...RequestOption) (*common.Block, error) {

	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryBlockByHash failed to prepare request parameters")
	}
	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	responses, err := c.ledger.QueryBlockByHash(reqCtx, blockHash, targets, c.verifier)
	if err != nil && len(responses) == 0 {
		return nil, errors.WithMessage(err, "QueryBlockByHash failed")
	}

	return matchBlockData(responses, opts.MinTargets)
}

// QueryBlockByTxID 根据交易id获取区块信息
func (c *Client) QueryBlockByTxID(txID ichain.TransactionID, options ...RequestOption) (*common.Block, error) {

	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryBlockByTxID failed to prepare request parameters")
	}
	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	responses, err := c.ledger.QueryBlockByTxID(reqCtx, txID, targets, c.verifier)
	if err != nil && len(responses) == 0 {
		return nil, errors.WithMessage(err, "QueryBlockByTxID failed")
	}

	return matchBlockData(responses, opts.MinTargets)
}

// QueryBlock 根据区块号查询区块信息
func (c *Client) QueryBlock(blockNumber uint64, options ...RequestOption) (*common.Block, error) {

	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryBlock failed to prepare request parameters")
	}
	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	responses, err := c.ledger.QueryBlock(reqCtx, blockNumber, targets, c.verifier)
	if err != nil && len(responses) == 0 {
		return nil, errors.WithMessage(err, "QueryBlock failed")
	}

	return matchBlockData(responses, opts.MinTargets)
}

func (c *Client) prepareRequestParams(options ...RequestOption) ([]ichain.Inode, *requestOptions, error) {
	opts, err := c.prepareRequestOpts(options...)
	if err != nil {
		return nil, nil, errors.WithMessage(err, "failed to get opts")
	}

	targets, err := c.calculateTargets(opts)
	if err != nil {
		return nil, nil, errors.WithMessage(err, "failed to determine target inodes")
	}

	return targets, &opts, nil
}

func matchBlockData(responses []*common.Block, minTargets int) (*common.Block, error) {
	if len(responses) < minTargets {
		return nil, errors.Errorf("Number of responses %d is less than MinTargets %d", len(responses), minTargets)
	}

	response := responses[0]
	for i, r := range responses {
		if i == 0 {
			continue
		}

		// Block data has to match
		if !proto.Equal(response.Data, r.Data) {
			return nil, errors.New("Block data does not match")
		}
	}

	return response, nil

}

// QueryTransaction 查询交易
func (c *Client) QueryTransaction(transactionID ichain.TransactionID, options ...RequestOption) (*pb.ProcessedTransaction, error) {

	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryTransaction failed to prepare request parameters")
	}
	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	responses, err := c.ledger.QueryTransaction(reqCtx, transactionID, targets, c.verifier)
	if err != nil && len(responses) == 0 {
		return nil, errors.WithMessage(err, "QueryTransaction failed")
	}

	if len(responses) < opts.MinTargets {
		return nil, errors.Errorf("QueryTransaction: Number of responses %d is less than MinTargets %d", len(responses), opts.MinTargets)
	}

	response := responses[0]
	for i, r := range responses {
		if i == 0 {
			continue
		}

		// All payloads have to match
		if !proto.Equal(response, r) {
			return nil, errors.New("Payloads for QueryTransaction do not match")
		}
	}

	return response, nil
}

// QueryConfig 查询链配置
func (c *Client) QueryConfig(options ...RequestOption) (ichain.ChainCfg, error) {

	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryConfig failed to prepare request parameters")
	}
	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	chainConfig, err := chconfig.NewChainConfigService(c.ctx.ChainID(), chconfig.WithInodes(targets), chconfig.WithMinResponses(opts.MinTargets))
	if err != nil {
		return nil, errors.WithMessage(err, "QueryConfig failed")
	}

	return chainConfig.Query(reqCtx)
}

// QueryConfigBlock 查询配置区块
func (c *Client) QueryConfigBlock(options ...RequestOption) (*common.Block, error) {
	targets, opts, err := c.prepareRequestParams(options...)
	if err != nil {
		return nil, errors.WithMessage(err, "QueryConfigBlock failed to prepare request parameters")
	}

	reqCtx, cancel := c.createRequestContext(opts)
	defer cancel()

	return c.ledger.QueryConfigBlock(reqCtx, targets, c.verifier)
}

// prepareRequestOpts 初始化请求参数，指定请求节点的个数
func (c *Client) prepareRequestOpts(options ...RequestOption) (requestOptions, error) {
	opts := requestOptions{}
	for _, option := range options {
		err := option(c.ctx, &opts)
		if err != nil {
			return opts, errors.WithMessage(err, "Failed to read request opts")
		}
	}

	// Set defaults for max targets
	if opts.MaxTargets == 0 {
		opts.MaxTargets = maxTargets
	}

	// Set defaults for min targets/matches
	if opts.MinTargets == 0 {
		opts.MinTargets = minTargets
	}

	if opts.MinTargets > opts.MaxTargets {
		opts.MaxTargets = opts.MinTargets
	}

	return opts, nil
}

// calculateTargets 获取请求目标节
func (c *Client) calculateTargets(opts requestOptions) ([]ichain.Inode, error) {

	if opts.Targets != nil && opts.TargetFilter != nil {
		return nil, errors.New("If targets are provided, filter cannot be provided")
	}

	targets := opts.Targets
	targetFilter := opts.TargetFilter

	var err error
	if targets == nil {
		targets, err = c.discovery.GetInodes()
		if err != nil {
			return nil, err
		}

		if targetFilter == nil {
			targetFilter = c.filter
		}
	}

	if targetFilter != nil {
		targets = filterTargets(targets, targetFilter)
	}

	if len(targets) == 0 {
		return nil, errors.WithStack(status.New(status.ClientStatus, status.NoInodesFound.ToInt32(), "no targets available", nil))
	}

	if len(targets) < opts.MinTargets {
		return nil, errors.Errorf("Error getting minimum number of targets. %d available, %d required", len(targets), opts.MinTargets)
	}

	numOfTargets := opts.MaxTargets
	if len(targets) < opts.MaxTargets {
		numOfTargets = len(targets)
	}

	shuffle(targets)

	return targets[:numOfTargets], nil
}

func (c *Client) createRequestContext(opts *requestOptions) (reqContext.Context, reqContext.CancelFunc) {

	if opts.Timeouts == nil {
		opts.Timeouts = make(map[ichain.TimeoutType]time.Duration)
	}

	if opts.Timeouts[ichain.InodeResponse] == 0 {
		opts.Timeouts[ichain.InodeResponse] = c.ctx.EndpointConfig().Timeout(ichain.InodeResponse)
	}

	return contextImpl.NewRequest(c.ctx, contextImpl.WithTimeout(opts.Timeouts[ichain.InodeResponse]), contextImpl.WithParent(opts.ParentContext))
}

func filterTargets(inodes []ichain.Inode, filter ichain.TargetFilter) []ichain.Inode {

	if filter == nil {
		return inodes
	}

	filteredInodes := []ichain.Inode{}
	for _, inode := range inodes {
		if filter.Accept(inode) {
			filteredInodes = append(filteredInodes, inode)
		}
	}

	return filteredInodes
}

func shuffle(a []ichain.Inode) {
	for i := range a {
		j := rand.Intn(i + 1)
		a[i], a[j] = a[j], a[i]
	}
}
